package org.wattdepot.simulation.oahugrid.main.realtime;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.datatype.XMLGregorianCalendar;
import org.wattdepot.client.BadXmlException;
import org.wattdepot.client.MiscClientException;
import org.wattdepot.client.NotAuthorizedException;
import org.wattdepot.client.ResourceNotFoundException;
import org.wattdepot.client.WattDepotClient;
import org.wattdepot.resource.property.jaxb.Property;
import org.wattdepot.resource.sensordata.jaxb.SensorData;
import org.wattdepot.resource.source.jaxb.Source;
import org.wattdepot.simulation.oahugrid.demandcurve.DailyDemandCurve;
import org.wattdepot.simulation.oahugrid.demandcurve.StandardDemandCurve;
import org.wattdepot.simulation.oahugrid.demandcurve.WeekendDemandCurve;
import org.wattdepot.simulation.oahugrid.powerplant.PowerPlant;
import org.wattdepot.simulation.oahugrid.powerplant.PowerPlantSet;
import org.wattdepot.simulation.oahugrid.strategy.ByPeakTypeStrategy;
import org.wattdepot.simulation.oahugrid.strategy.SimulationEntry;
import org.wattdepot.simulation.oahugrid.strategy.SimulationStrategy;
import org.wattdepot.simulation.util.SimulationClient;
import org.wattdepot.util.tstamp.Tstamp;
import it.sauronsoftware.cron4j.TaskExecutionContext;
import it.sauronsoftware.cron4j.Task;

import static org.wattdepot.simulation.oahugrid.powerplant.PowerPlantSet.HIGH_CARBON_PLANTS;
import static org.wattdepot.simulation.oahugrid.powerplant.PowerPlantSet.LOW_CARBON_PLANTS;

/**
 * Determines the power plants and their values each time the cron job wakes up.
 * @author Philip Johnson
 *
 */
public class RealtimeSimulationTask extends Task {
  /** Maps the day of the year to its associated demandCurve. */
  private Map<Integer, DailyDemandCurve> demandCurveCache = new HashMap<Integer,DailyDemandCurve>();
  /** Maps the day of the year to its associated plantSet. */
  private Map<Integer, PowerPlantSet> plantSetCache = new HashMap<Integer,PowerPlantSet>();
  /** Maps plant names to their associated WattDepotClient. */
  private WattDepotClient client;
  
  
  /**
   * Creates the simulation task, and reads in properties from wattdepot.simulation.properties
   * which should be located in ~/.wattdepot/client.  If not found, or if the required properties 
   * are not found, then system exits with an error. 
   */
  public RealtimeSimulationTask() {
    System.out.println("Getting simulation properties and initializing the WattDepot client.");
    this.client = SimulationClient.getWattDepotClient();
    
    System.out.println("Validating PowerPlant sources and getting EnergyGeneratedToDate (MWH).");
    for (PowerPlant plant : PowerPlant.values()) {
      this.initializePowerPlant(plant);
    }
  }

  /**
   * Determine the power plants and their power/energy values upon wakeup.
   * @param context The task execution context (ignored).
   * @throws RuntimeException If problems occur. 
   */
  @Override
  public void execute(TaskExecutionContext context) throws RuntimeException {
    System.out.println("Oahu grid simulation waking up at: " + new Date());
    GregorianCalendar now = new GregorianCalendar();
    int dayOfYear = now.get(Calendar.DAY_OF_YEAR);
    // Determine what power plants are running now.
    PowerPlantSet set = getPowerPlantSet(dayOfYear);
    // Determine the daily demand curve for today.
    DailyDemandCurve curve = getDemandCurve(now); 
    // Specify the strategy by which we will allocated power among plants. 
    SimulationStrategy strategy = new ByPeakTypeStrategy();
    // Determine the current 15 minute interval that we are in.
    int interval = this.getCurrentInterval(now);
    // Get the simulation entries indicating the power associated with each plant for this interval.
    List<SimulationEntry> entries = strategy.generateInterval(curve, set, interval);
    long totalWatts = 0;
    for (SimulationEntry entry : entries) {
      PowerPlant plant = entry.getPlant();
      String name = plant.name();
      long watts = 1000000L * entry.getGridMW();
      totalWatts += watts;
      double energyGeneratedToDate = plant.getEnergyGeneratedToDate() + (watts / 4.0);
      plant.incrementEnergyGeneratedToDate(watts / 4);
      System.out.format("  Plant: %15s, Power: %3.1f MW, Energy Generated To Date: %4.1f MWH%n", 
          name, mega(watts), mega(energyGeneratedToDate));
      // Send data to WattDepot server.
      sendData(plant, watts, energyGeneratedToDate);
      // Update each plant's energy-to-date counter to include the next 15 min of power generation.
    }
    // Indicate the total MW of power the plants are generating during this time interval.
    System.out.format("  Total power: %3.1f MW%n", mega(totalWatts));
  }
  
  /**
   * Send the simulation data to the WattDepot server.
   * Prints an error message if problems occur, but does not exit.  
   * @param plant The plant (the WattDepot source).
   * @param watts The current power.
   * @param energyGeneratedToDate Its current cumulative energy.
   */
  private void sendData(PowerPlant plant, double watts, double energyGeneratedToDate) {
    XMLGregorianCalendar timestamp = Tstamp.makeTimestamp();
    String toolName = "OahuGridSimulation";
    String sourceURI = Source.sourceToUri(plant.name(), client.getWattDepotUri());
    SensorData data = new SensorData(timestamp, toolName, sourceURI);
    data.addProperty(new Property(SensorData.POWER_GENERATED, watts));
    data.addProperty(new Property(SensorData.ENERGY_GENERATED_TO_DATE, energyGeneratedToDate));
    try {
      client.storeSensorData(data);
    }
    catch (Exception e) {
      System.out.format("Error while sending data to WattDepot: %n  %s" + e.getMessage());
    }
  }



  /**
   * Returns an ordered list of the power plants. The list will either  
   * prefer high carbon plants (on even days) or low carbon plants (on odd days).
   * We cache the value upon first computation for convenience.
   * @param dayOfYear The day of the year of interest. 
   * @return The ordered list of power plants. 
   */
  private PowerPlantSet getPowerPlantSet(int dayOfYear) {
    // Update the cache if we don't have an entry.
    if (!plantSetCache.containsKey(dayOfYear)) {
      boolean preferHighCarbon = (dayOfYear % 2) == 0;
      PowerPlantSet set = preferHighCarbon ? HIGH_CARBON_PLANTS : LOW_CARBON_PLANTS;
      plantSetCache.put(dayOfYear, set);
    }
    // Return the power plant set from the cache.
    return plantSetCache.get(dayOfYear);
  }
  
  /**
   * Returns a DailyDemandCurve given a day. Currently, the demand curve is based just on the day of
   * the week; we choose either the StandardDemandCurve or the WeekendDemandCurve. The weekend has a
   * 5 MW jiggle, the weekday has a 10 MW jiggle.
   * 
   * @param day The day for which we want a demand curve.
   * @return The DailyDemandCurve to be used for this day.
   */
  private DailyDemandCurve getDemandCurve(GregorianCalendar day) {
    int dayOfYear = day.get(Calendar.DAY_OF_YEAR);
    // Update the cache if we don't have an entry.
    if (!demandCurveCache.containsKey(dayOfYear)) {
      // If it's a weekend, generate data using the weekend demand curve.
      if ((day.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY)
          || (day.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY)) {
        demandCurveCache.put(dayOfYear, new WeekendDemandCurve(day).addRandomVariation(5));
      }
      else {
        demandCurveCache.put(dayOfYear, new StandardDemandCurve(day).addRandomVariation(10));
      }
    }
    // Return the demand curve from the cache.
    return demandCurveCache.get(dayOfYear);
  }
  

  /**
   * Returns an integer between 0 and 95 indicating the current 15 minute interval associated
   * with the passed timestamp.
   * @param today The timestamp of interest. 
   * @return An integer between 0 and 95.
   */
  private int getCurrentInterval(GregorianCalendar today) {
    int hourOfDay = today.get(Calendar.HOUR_OF_DAY);
    int minuteOfDay = today.get(Calendar.MINUTE);
    return (hourOfDay * 4) + (minuteOfDay / 15);
  }
  
  /**
   * Ensures that the passed PowerPlant is a valid source on the server. 
   * Then retrieves the energy generated to date value.  If no sensor data for that source,
   * it initializes energy generated to date to zero.
   * @param plant The plant that should correspond to a source. 
   */
  private void initializePowerPlant(PowerPlant plant) {
    // First, validate the source. 
    try {
      this.client.getSource(plant.name());
    }
    catch (NotAuthorizedException e) {
      System.out.format("You do not have access to the source %s. Aborting.%n", plant.name());
      throw new RuntimeException(e);
    }
    catch (ResourceNotFoundException e) {
      System.err.format("Source %s does not exist on server. Aborting.%n", plant.name());
      throw new RuntimeException(e);
    }
    catch (BadXmlException e) {
      System.err.println("Received bad XML from server, which is weird. Aborting.");
      throw new RuntimeException(e);
    }
    catch (MiscClientException e) {
      System.err.println("Had problems retrieving source from server, which is weird. Aborting.");
      throw new RuntimeException(e);
    }
    // Now, initialize its energy generated value. 
    double latestEnergy = 0;
    try {
      latestEnergy = this.client.getLatestEnergyGeneratedToDate(plant.name());
    }
    catch (ResourceNotFoundException e) {
      System.out.format("No prior energy generation found for source %s%n", plant.name());
    }
    catch (Exception e) {
      System.err.format("Had problems retrieving energy to date for source %s.%n", plant.name());
      throw new RuntimeException(e);
    }
    System.out.format("%15s energy generated to date: %4.1f MWH%n", plant.name(), 
        mega(latestEnergy));
    plant.initializeEnergyGeneratedToDate(latestEnergy);

  }
  
  /**
   * Converts the passed watts (or watt-hours) to megawatts (or megawatt-hours).
   * @param watts The watts.
   * @return The mega version. 
   */
  
  private double mega(double watts) {
    return (watts / 1000000);
  }
}
